package erfgame.core.world.terrain.update;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;


import erfgame.core.Point3D;
import erfgame.core.Point3DAndRotation;
import erfgame.core.Rectangle3D;
import erfgame.core.Rotation;
import erfgame.core.dao.ObjectDAO;
import erfgame.core.graph.simple.Node;
import erfgame.core.graphics.renderer.RendererContext;
import erfgame.core.memory.Pointer;
import erfgame.core.util.RangeUtils;
import erfgame.core.world.World;
import erfgame.core.world.terrain.BlockSurfaceImageFactory;
import erfgame.core.world.terrain.BlockSurfaceImages;
import erfgame.core.world.terrain.PixelDepth;
import erfgame.core.world.terrain.SurfaceImage;
import erfgame.core.world.terrain.TerrainConstants;
import erfgame.core.world.terrain.TerrainUpdate;
import erfgame.core.world.terrain.BlockSurfaceImageFactory.BoundsAndGridId;
import erfgame.core.world.terrain.insertion.SurfaceInsertion;
import erfgame.core.world.terrain.insertion.SurfaceTerrainImageInsertion;
import erfgame.core.world.terrain.rotated.RotatedWorld;
import erfgame.core.world.terrain.rotated.TransformedBlock;

public class BlockSurfaceImageUpdater {
	
	private static final Logger log = Logger.getLogger( BlockSurfaceImageUpdater.class.getName() );
	
	private int sampleBuffer;
	private ObjectDAO<Point3D, Pointer<byte[][][]>> terrainDAO;
	private ObjectDAO<Point, int[][][]> surfacesDAO;
	
	public BlockSurfaceImageUpdater( ObjectDAO<Point3D, Pointer<byte[][][]>> terrainDAO, ObjectDAO<Point, int[][][]> surfacesDAO, int sampleBuffer ) {
		this.sampleBuffer = sampleBuffer;
		this.terrainDAO = terrainDAO;
		this.surfacesDAO = surfacesDAO;
	}
	
	public void updateBlockSurfaceImages(
			World world,
			TerrainUpdate terrainUpdate, 
			int worldX, 
			int worldY, 
			int worldZ, 
			Rotation rotation,
			TerrainBlockUpdateResult terrainUpdateResult,
			ObjectDAO<Point3DAndRotation, BlockSurfaceImages> bsiDAO, 
			BlockSurfaceImageFactory bsiFactory
	) {
	
		RotatedWorld rotatedWorld = new RotatedWorld( world, rotation );
	
		Rectangle3D sampleBounds = terrainUpdateResult.getSampleBounds();
		
		
		Rectangle3D originalCoverage = terrainUpdateResult.getCoverage();
		Rectangle3D coverage = new Rectangle3D( 
				originalCoverage.minX - sampleBuffer, 
				originalCoverage.minY - sampleBuffer, 
				originalCoverage.minZ - sampleBuffer, 
				originalCoverage.maxX + sampleBuffer, 
				originalCoverage.maxY + sampleBuffer, 
				originalCoverage.maxZ + sampleBuffer
		);
		
		byte[][][][][][] allTerrain = terrainUpdateResult.getSample();
		int[][][][][] allSurfaces = terrainUpdateResult.getSurfaces();
		
		updateBlockSurfaceImages(
				rotatedWorld, 
				sampleBounds, 
				worldX, 
				worldY, 
				worldZ, 
				terrainUpdate.getWidth(), 
				terrainUpdate.getHeight(),
				terrainUpdate.getDepth(),
				coverage, 
				allTerrain, 
				allSurfaces, 
				bsiDAO, 
				bsiFactory, 
				sampleBuffer, 
				true
		);
		
		// update for the shadow
		int minShadowZ = terrainUpdateResult.getHighestUnderTerrainZ();
		int maxShadowZ = terrainUpdateResult.getLowestUnderTerrainZ();
		if( minShadowZ <= maxShadowZ && terrainUpdateResult.getShadowPointers() != null ) {
			Rectangle3D shadowCoverage = new Rectangle3D( 
					originalCoverage.minX, 
					originalCoverage.minY, 
					minShadowZ, 
					originalCoverage.maxX, 
					originalCoverage.maxY, 
					maxShadowZ
			);
			Rectangle3D shadowSampleBounds = new Rectangle3D(
					sampleBounds.minX, 
					sampleBounds.minY, 
					terrainUpdateResult.getMinShadowSampleZ(), 
					sampleBounds.maxX, 
					sampleBounds.maxY, 
					terrainUpdateResult.getMaxShadowSampleZ() 
			);
			// TODO needs to pass in the minimum pixel height under which we don't need to update (because it's already updated as part of the terrain update) 
			updateBlockSurfaceImages(
					rotatedWorld, 
					shadowSampleBounds, 
					worldX, 
					worldY, 
					minShadowZ, 
					terrainUpdate.getWidth(),
					terrainUpdate.getHeight(), 
					maxShadowZ - minShadowZ+1, 
					shadowCoverage, 
					terrainUpdateResult.getShadowSample(), 
					allSurfaces,
					bsiDAO, 
					bsiFactory, 
					0, 
					false
			);
			log.info("rendered a separate shadow");
		} else {
			log.info("didn't render a shadow");
		}
	}
	
	
	@SuppressWarnings("unchecked")
	private void updateBlockSurfaceImages(
			RotatedWorld rotatedWorld, 
			Rectangle3D sampleBounds,
			int worldX, 
			int worldY, 
			int worldZ, 
			int terrainUpdateWidth, 
			int terrainUpdateHeight, 
			int terrainUpdateDepth, 
			Rectangle3D coverage, 
			byte[][][][][][] allTerrain, 
			int[][][][][] allSurfaces, 
			ObjectDAO<Point3DAndRotation, BlockSurfaceImages> bsiDAO, 
			BlockSurfaceImageFactory bsiFactory, 
			int sampleBuffer, 
			boolean updateDepths

	) { 
		Rotation rotation = rotatedWorld.getRotation();
		Rotation oppositeRotation = rotation.getOpposite();
		Point tempPoint = new Point();
		World world = rotatedWorld.getOriginalWorld();
		
		int sampleWidth = sampleBounds.getWidth();
		int sampleHeight = sampleBounds.getHeight();
		int sampleDepth = sampleBounds.getDepth();

		int blockWidth = world.getBlockWidth();
		int blockHeight = world.getBlockHeight();
		int blockDepth = world.getBlockDepth();

		int terrainWidth = terrainUpdateWidth + sampleBuffer * 2;
		int terrainHeight = terrainUpdateHeight + sampleBuffer * 2;
		int terrainDepth = terrainUpdateDepth + sampleBuffer * 2;
		
		int terrainX = worldX - sampleBuffer;
		int terrainY = worldY - sampleBuffer;
		int terrainZ = worldZ - sampleBuffer;
		
		int startBlockX = Math.max( 0, terrainX >> world.getBlockWidthShift() );
		int startBlockY = Math.max( 0, terrainY >> world.getBlockHeightShift() );
		int startBlockZ = Math.max( 0, terrainZ / blockDepth );
		int finalBlockX = Math.min( world.getBlocksX()-1, (terrainX + terrainWidth - 1)>>world.getBlockWidthShift() );
		int finalBlockY = Math.min( world.getBlocksY()-1, (terrainY + terrainHeight - 1)>>world.getBlockHeightShift() );
		int finalBlockZ = Math.min( world.getBlocksZ()-1, (terrainZ + terrainDepth - 1)/blockDepth );

		int blockOffsetX = startBlockX - sampleBounds.minX;
		int blockOffsetY = startBlockY - sampleBounds.minY;
		int blockOffsetZ = startBlockZ - sampleBounds.minZ;
		

		// convert to rotated terrain block
		TransformedBlock<TransformedBlock<byte[]>[]> rotatedTerrain;
		TransformedBlock<byte[]>[][][] rotatedTerrainParts = new TransformedBlock[sampleBounds.getWidth()][sampleBounds.getHeight()][sampleBounds.getDepth()];
		for( int x = sampleBounds.getWidth(); x>0; ) {
			x--;
			for( int y = sampleBounds.getHeight(); y>0; ) {
				y--;
				for( int z = sampleBounds.getDepth(); z>0; ) {
					z--;
					rotatedTerrainParts[x][y][z] = new TransformedBlock<byte[]>(
						allTerrain[x][y][z], 
						rotation, 
						rotation.getRotateInBoxTransformation(blockWidth-1, blockHeight-1), 
						blockWidth, 
						blockHeight
					);
				}
			}
		}
		
		rotatedTerrain = new TransformedBlock<TransformedBlock<byte[]>[]>(
				rotatedTerrainParts,
				sampleBounds.getWidth(), 
				sampleBounds.getHeight()
		);
		
		for( int blockX = startBlockX; blockX <= finalBlockX; blockX++ ) {
			int bx = blockX - startBlockX;
			int cbx = bx + blockOffsetX;
			int blockWorldX = blockX << world.getBlockWidthShift();
			for( int blockY = startBlockY; blockY <= finalBlockY; blockY++ ) {
				int by = blockY - startBlockY;
				int cby = by + blockOffsetY;
				Point midPoint = new Point( cbx << 1, cby << 1 ); 
				// rotate in box does not work for this!!!!
				rotatedTerrain.setRotation( 
						rotation, 
						rotation.getRotationTransformation( midPoint ) 
				);

				int blockWorldY = blockY << world.getBlockHeightShift();
				for( int blockZ = startBlockZ; blockZ <= finalBlockZ; blockZ++ ) {
					int bz = blockZ - startBlockZ;
					int cbz = bz + blockOffsetZ;
					// re-render the block surface images
					// if only a portion of the block has changed
					// only render the parts that have changed
					int blockWorldZ = blockZ * blockDepth;
					boolean needsRewrite;
					BlockSurfaceImages bsis;
					Rectangle3D bounds = new Rectangle3D(
							blockWorldX, 
							blockWorldY, 
							blockWorldZ, 
							blockWorldX + world.getBlockWidth()-1,
							blockWorldY + world.getBlockHeight()-1, 
							blockWorldZ + world.getBlockDepth()-1
					);
					bounds.intersect(coverage);
					if( !bounds.isEmpty() ) {
						Point3DAndRotation key = new Point3DAndRotation( blockX, blockY, blockZ, rotation );
						
						if( bounds.getWidth() < blockWidth || 
								bounds.getHeight() < blockHeight ||
								bounds.getDepth() < blockDepth 
						) {
							
							Rectangle3D rotatedBounds = bounds;
							bounds.minX -= blockWorldX;
							bounds.minY -= blockWorldY;
							bounds.minZ -= blockWorldZ;
							bounds.maxX -= blockWorldX;
							bounds.maxY -= blockWorldY;
							bounds.maxZ -= blockWorldZ;
							// TODO : rotate in box
							rotatedBounds.rotate(oppositeRotation, world.getBlockMidPointTimes2(), tempPoint);
							
							// make sure we've got an even number of lines to avoid off-by one errors
							// this needs to factor in rotation
							rotatedBounds.maxY = rotatedBounds.maxY + rotatedBounds.maxY % TerrainConstants.Y_SPACING;
							
							byte[][][] terrain = allTerrain[ cbx ][ cby ][ cbz ];
							
							int pixelX = rotatedBounds.minX;
							int pixelY = BlockSurfaceImageFactory.getPixelY( 
									rotatedBounds.minY,
									rotatedBounds.minZ
							);
							int pixelWidth = rotatedBounds.getWidth();
							int pixelHeight = BlockSurfaceImageFactory.getPixelY(
									rotatedBounds.getHeight(), 
									rotatedBounds.getDepth()
							);
							
							// only rewrite a portion
							bsis = bsiDAO.retrieveObject(
									key
							);
	
							PixelDepth[][] intoDepths = bsis.getPixelDepths();
							if( intoDepths == null ) {
								intoDepths = new PixelDepth[world.getBlockWidth()][BlockSurfaceImageFactory.getPixelY(world.getBlockHeight(), world.getBlockDepth())];
								bsis.setPixelDepths(intoDepths);
							}
							int[][] intoSurfaces = bsis.getSurfaces();
							if( intoSurfaces == null ) {
								intoSurfaces = new int[world.getBlockWidth()][BlockSurfaceImageFactory.getPixelY(world.getBlockHeight(), world.getBlockDepth())];
								bsis.setSurfaces(intoSurfaces);
							} else {
								// zero out the depths and surfaces
								for( int px=pixelX; px<pixelX+pixelWidth; px++ ) {
									// TODO : use a pre-generated array of empty values and do array copy
									for( int py=pixelY; py<pixelY+pixelHeight; py++ ) {
										// TODO : dislike checking this every time
										if( updateDepths ) {
											intoDepths[px][py] = null;
										}
										intoSurfaces[px][py] = 0;
									}
								}
							}
							// recreate the depths and surfaces
							// TODO : surface map and pixel depth map don't need to use the additional buffer 
							// area (use original coverage)
							TransformedBlock<byte[]> transformedBlock = new TransformedBlock<byte[]>(
									terrain, 
									rotation, 
									rotation.getRotateInBoxTransformation(
											blockWidth-1, 
											blockHeight-1
									),
									blockWidth, 
									blockHeight 									
							);
							if( updateDepths ) {
								bsiFactory.buildPixelDepthMap(
										pixelX,
										pixelY,
										pixelWidth, 
										pixelHeight, 
										intoDepths, 
										blockDepth, 
										transformedBlock
								);
							}
							ArrayList<BoundsAndGridId> mapBounds = bsiFactory.buildSurfaceMap(
									pixelX, 
									pixelY,
									pixelWidth, 
									pixelHeight, 
									intoSurfaces, 
									intoDepths, 
									bsis.getNextSurfaceId()
							);
							if( 
									bsis.getNextSurfaceId() + mapBounds.size() > BlockSurfaceImages.MAX_SURFACES || 
									bsis.getSurfaceInsertions().size() + mapBounds.size() > BlockSurfaceImages.MAX_SURFACES
							) {
								// too many separate images, try and consolidate
								needsRewrite = true;
							} else {
								needsRewrite = false;
								HashSet<RendererContext> contexts = new HashSet<RendererContext>(1);
								List<Node<SurfaceInsertion>> insertions = bsis.getSurfaceInsertions();
								// blank out the overlapping parts of the existing surface images with transparency
								for( int i=insertions.size(); i>0; ) {
									i--;
									SurfaceInsertion insertion = insertions.get( i ).getValue();
									if( insertion instanceof SurfaceTerrainImageInsertion ) {
										SurfaceImage surfaceImage = ((SurfaceTerrainImageInsertion)insertion).getSurfaceImage();
										
										int six = surfaceImage.getPx();
										int siy = surfaceImage.getPy();
										BufferedImage image = surfaceImage.getImage();
										// see if there is overlap
										int[] xoverlap = RangeUtils.intersection( 
												0, 
												image.getWidth(), 
												pixelX - six, 
												pixelX - six + pixelWidth
										);
										int minx = xoverlap[0];
										int maxx = xoverlap[1];
										if( minx < maxx ) {
											int[] yoverlap = RangeUtils.intersection(
													0, 
													image.getHeight(), 
													pixelY - siy, 
													pixelY - siy + pixelHeight
											);
											int miny = yoverlap[0];
											int maxy = yoverlap[1];
											if( miny < maxy ) {
												contexts.addAll(insertion.getRendererChain().getContexts());
												//if( xoverlap[0] == 0 && xoverlap[1] == image.getWidth() && yoverlap[0] == 0 && yoverlap[1] == image.getHeight() ) {
												if( false ) {
													// TODO discard this image, it's been redrawn entirely
												} else {
													for( int sx = minx; sx<maxx; sx++ ) {
														for( int sy = miny; sy<maxy; sy++ ) {
															image.setRGB( sx, sy, 0x00000000 );
														}
													}
												}
											}
										}
									}
								}
								ArrayList<SurfaceInsertion> newInsertions = new ArrayList<SurfaceInsertion>(mapBounds.size());
								for( int i=mapBounds.size(); i>0; ) {
									i--;
									BoundsAndGridId bag = mapBounds.get( i ); 
									Rectangle mapBound = bag.bounds;
									int surfaceId = bsis.getNextSurfaceId() + i;
									BufferedImage image = new BufferedImage( 
											mapBound.width, 
											mapBound.height,
											BufferedImage.TYPE_INT_ARGB
									);
									// TODO should this be x + width - 1??
									
									SurfaceImage si = bsiFactory.renderSurfaceImage(
											mapBound.x, 
											mapBound.x + mapBound.width, 
											mapBound.y, 
											mapBound.y + mapBound.height, 
											intoDepths, 
											surfaceId,
											intoSurfaces, 
											allSurfaces,
											blockWorldX, 
											blockWorldY, 
											blockWorldZ, 
											rotatedWorld, 
											cbx, 
											cby, 
											cbz, 
											rotatedTerrain, 
											allTerrain,
											image,
											bag.blockGridId,
											blockZ == 0
									);
									SurfaceTerrainImageInsertion newInsertion;
									newInsertion = new SurfaceTerrainImageInsertion(bsis, si);
									newInsertion.getRendererChain().attach(contexts);
									newInsertions.add( newInsertion );
								}
								bsis.addInsertions( newInsertions );
								bsis.setNextSurfaceId( bsis.getNextSurfaceId() + mapBounds.size() );
							}
						} else {
							// rewrite the whole thing
							needsRewrite = true;
							bsis = null;
						}
						if( needsRewrite ) {
							// TODO pixel depths should already be accurate, don't need to recalculate
							
							// if it hasn't already, this needs to get all the surrounding blocks, 
							// not just the ones that are in the buffer
							// currently leaves lines on the block border, maybe should be user-selectable setting
							
							if( cbx == 0 || cby == 0 || cbz == 0 || cbx == sampleWidth-1 || cby == sampleHeight-1 || cbz == sampleDepth-1 ) {
								log.log(Level.INFO, "inefficient update of entire block at "+rotation.getDegrees()+" ("+blockX+","+blockY+","+blockZ+")");
								// shocking waste of already loaded values, although the cache should catch this
								ArrayList<Pointer<byte[][][]>> pointers = new ArrayList<Pointer<byte[][][]>>(27);
								TransformedBlock<byte[]>[][][] surroundsBlocks = new TransformedBlock[3][3][3];
								byte[][][][][][] surrounds = new byte[3][3][3][][][];
								int[][][][][] surroundsSurfaces = new int[3][3][][][];
								for( int x=3; x>0; ) {
									x--;
									int sbx = blockX + x - 1;
									if( sbx >= 0 && sbx < world.getBlocksX() ) {
										for( int y=3; y>0; ) {
											y--;
											int sby = blockY + y - 1;
											if( sby >= 0 && sby < world.getBlocksY() ) {
												for( int z=3; z>0; ) {
													z--;
													int sbz = blockZ + z - 1;
													if( sbz >= 0 && sbz < world.getBlocksZ() ) {
														Point3D skey = new Point3D( sbx, sby, sbz );
														Pointer<byte[][][]> pointer = terrainDAO.retrieveObject(skey);
														pointers.add( pointer );
														byte[][][] terrain = pointer.getValue();
														surrounds[x][y][z] =  terrain;
														surroundsBlocks[x][y][z] = new TransformedBlock<byte[]>(
																terrain, 
																rotation, 
																rotation.getRotateInBoxTransformation(blockWidth-1, blockHeight-1), 
																blockWidth, 
																blockHeight
														);
													}
												}
												surroundsSurfaces[x][y] = surfacesDAO.retrieveObject(new Point(sbx, sby));
											}
										}
									}
								}
								bsis = bsiFactory.render(
										1, 
										1, 
										1,
										new TransformedBlock<TransformedBlock<byte[]>[]>(
												surroundsBlocks, 
												rotation, 
												rotation.getRotateInBoxTransformation(2, 2), 
												3, 
												3
										),
										surrounds,
										surroundsSurfaces,
										new Point3D( blockX, blockY, blockZ ),
										rotatedWorld, 
										blockZ == 0
								);
								// free the pointers
								for( int i=pointers.size(); i>0; ) {
									i--;
									pointers.get(i).free();
								}
							} else {
								log.log(Level.INFO, "update of entire block at "+rotation.getDegrees()+" ("+blockX+","+blockY+","+blockZ+")");
								bsis = bsiFactory.render(
										cbx, 
										cby, 
										cbz,
										rotatedTerrain,
										allTerrain,
										allSurfaces,
										new Point3D( blockX, blockY, blockZ ),
										rotatedWorld, 
										blockZ == 0
								);
							}
						}
//						System.out.println( "storing "+key );
						bsiDAO.storeObject(
								key, 
								bsis
						);
					}
				}
			}
		}

	}
}
